{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "qFjEbFtUWGB-"
      },
      "source": [
        "<font color=\"red\">注</font>: 使用 tensorboard 可视化需要安装 tensorflow (TensorBoard依赖于tensorflow库，可以任意安装tensorflow的gpu/cpu版本)\n",
        "\n",
        "```shell\n",
        "pip install tensorflow-cpu\n",
        "```"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "lmMat_siWGCA",
        "outputId": "aa737bdd-41cd-41f1-aa11-a7c5183bcfaf"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "sys.version_info(major=3, minor=11, micro=11, releaselevel='final', serial=0)\n",
            "matplotlib 3.10.0\n",
            "numpy 1.26.4\n",
            "pandas 2.2.2\n",
            "sklearn 1.6.0\n",
            "torch 2.5.1+cu121\n",
            "cuda:0\n"
          ]
        }
      ],
      "source": [
        "import matplotlib as mpl\n",
        "import matplotlib.pyplot as plt\n",
        "%matplotlib inline\n",
        "import numpy as np\n",
        "import sklearn\n",
        "import pandas as pd\n",
        "import os\n",
        "import sys\n",
        "import time\n",
        "from tqdm.auto import tqdm\n",
        "import torch\n",
        "import torch.nn as nn\n",
        "import torch.nn.functional as F\n",
        "\n",
        "print(sys.version_info)\n",
        "for module in mpl, np, pd, sklearn, torch:\n",
        "    print(module.__name__, module.__version__)\n",
        "\n",
        "device = torch.device(\"cuda:0\") if torch.cuda.is_available() else torch.device(\"cpu\")\n",
        "print(device)\n",
        "\n",
        "seed = 42\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "sLVu9p5IWGCB"
      },
      "source": [
        "## 数据准备"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "background_save": true,
          "base_uri": "https://localhost:8080/"
        },
        "id": "5Jk8ZKZnWGCB",
        "outputId": "8d15d4a9-7d3c-4835-8b08-458e56866261"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Downloading http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/train-images-idx3-ubyte.gz\n",
            "Downloading http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/train-images-idx3-ubyte.gz to data/FashionMNIST/raw/train-images-idx3-ubyte.gz\n"
          ]
        },
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "100%|██████████| 26.4M/26.4M [00:01<00:00, 13.2MB/s]\n"
          ]
        },
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Extracting data/FashionMNIST/raw/train-images-idx3-ubyte.gz to data/FashionMNIST/raw\n",
            "\n",
            "Downloading http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/train-labels-idx1-ubyte.gz\n",
            "Downloading http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/train-labels-idx1-ubyte.gz to data/FashionMNIST/raw/train-labels-idx1-ubyte.gz\n"
          ]
        },
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "100%|██████████| 29.5k/29.5k [00:00<00:00, 215kB/s]\n"
          ]
        },
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Extracting data/FashionMNIST/raw/train-labels-idx1-ubyte.gz to data/FashionMNIST/raw\n",
            "\n",
            "Downloading http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/t10k-images-idx3-ubyte.gz\n",
            "Downloading http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/t10k-images-idx3-ubyte.gz to data/FashionMNIST/raw/t10k-images-idx3-ubyte.gz\n"
          ]
        },
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "100%|██████████| 4.42M/4.42M [00:01<00:00, 3.91MB/s]\n"
          ]
        },
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Extracting data/FashionMNIST/raw/t10k-images-idx3-ubyte.gz to data/FashionMNIST/raw\n",
            "\n",
            "Downloading http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/t10k-labels-idx1-ubyte.gz\n",
            "Downloading http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/t10k-labels-idx1-ubyte.gz to data/FashionMNIST/raw/t10k-labels-idx1-ubyte.gz\n"
          ]
        },
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "100%|██████████| 5.15k/5.15k [00:00<00:00, 7.01MB/s]"
          ]
        },
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Extracting data/FashionMNIST/raw/t10k-labels-idx1-ubyte.gz to data/FashionMNIST/raw\n",
            "\n"
          ]
        },
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "\n"
          ]
        }
      ],
      "source": [
        "from torchvision import datasets\n",
        "from torchvision.transforms import ToTensor\n",
        "from torch.utils.data import random_split\n",
        "\n",
        "# fashion_mnist图像分类数据集\n",
        "train_ds = datasets.FashionMNIST(\n",
        "    root=\"data\",\n",
        "    train=True,\n",
        "    download=True,\n",
        "    transform=ToTensor()\n",
        ")\n",
        "\n",
        "test_ds = datasets.FashionMNIST(\n",
        "    root=\"data\",\n",
        "    train=False,\n",
        "    download=True,\n",
        "    transform=ToTensor()\n",
        ")\n",
        "\n",
        "# torchvision 数据集里没有提供训练集和验证集的划分\n",
        "# 这里用 random_split 按照 11 : 1 的比例来划分数据集\n",
        "train_ds, val_ds = random_split(train_ds, [55000, 5000], torch.Generator().manual_seed(seed))"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "ogbQDnVxWGCC"
      },
      "outputs": [],
      "source": [
        "from torchvision.transforms import Normalize\n",
        "\n",
        "# 遍历train_ds得到每张图片，计算每个通道的均值和方差\n",
        "def cal_mean_std(ds):\n",
        "    mean = 0.\n",
        "    std = 0.\n",
        "    for img, _ in ds:\n",
        "        mean += img.mean(dim=(1, 2))\n",
        "        std += img.std(dim=(1, 2))\n",
        "    mean /= len(ds)\n",
        "    std /= len(ds)\n",
        "    return mean, std\n",
        "\n",
        "\n",
        "# print(cal_mean_std(train_ds))\n",
        "# 0.2860， 0.3205\n",
        "transforms = nn.Sequential(\n",
        "    Normalize([0.2856], [0.3202])\n",
        ") # 对每个通道进行标准化"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "muj76MivWGCC",
        "outputId": "fec5cc6d-bad4-405d-9235-815e9cd900b2"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "(torch.Size([1, 28, 28]), 9)"
            ]
          },
          "execution_count": 4,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "img, label = train_ds[0]\n",
        "img.shape, label"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "aZsO4fXOWGCC",
        "outputId": "6a164899-e768-4746-d418-1fef0810b1ed"
      },
      "outputs": [
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "/usr/local/lib/python3.11/dist-packages/torch/utils/data/dataloader.py:617: UserWarning: This DataLoader will create 4 worker processes in total. Our suggested max number of worker in current system is 2, which is smaller than what this DataLoader is going to create. Please be aware that excessive worker creation might get DataLoader running slow or even freeze, lower the worker number to avoid potential slowness/freeze if necessary.\n",
            "  warnings.warn(\n"
          ]
        }
      ],
      "source": [
        "from torch.utils.data.dataloader import DataLoader\n",
        "\n",
        "batch_size = 32\n",
        "# 从数据集到dataloader\n",
        "train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=4)\n",
        "val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False, num_workers=4)\n",
        "test_loader = DataLoader(test_ds, batch_size=batch_size, shuffle=False, num_workers=4)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "lNVkpePHWGCC"
      },
      "source": [
        "## 定义模型"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "v7H82XkQWGCC",
        "outputId": "7cc17996-9e63-4ed2-9787-d3238f876f6c"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "1152"
            ]
          },
          "execution_count": 6,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "128*9"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "GlRkiG1iWGCD",
        "outputId": "b2255543-8f57-4589-cd17-a44aaf5e8e90"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "conv1.weight\tparamerters num: 288\n",
            "conv1.bias\tparamerters num: 32\n",
            "conv2.weight\tparamerters num: 9216\n",
            "conv2.bias\tparamerters num: 32\n",
            "conv3.weight\tparamerters num: 18432\n",
            "conv3.bias\tparamerters num: 64\n",
            "conv4.weight\tparamerters num: 36864\n",
            "conv4.bias\tparamerters num: 64\n",
            "conv5.weight\tparamerters num: 73728\n",
            "conv5.bias\tparamerters num: 128\n",
            "conv6.weight\tparamerters num: 147456\n",
            "conv6.bias\tparamerters num: 128\n",
            "fc1.weight\tparamerters num: 147456\n",
            "fc1.bias\tparamerters num: 128\n",
            "fc2.weight\tparamerters num: 1280\n",
            "fc2.bias\tparamerters num: 10\n"
          ]
        }
      ],
      "source": [
        "# class CNN(nn.Module):\n",
        "#     def __init__(self, activation=\"relu\"):\n",
        "#         super(CNN, self).__init__()\n",
        "#         self.activation = F.relu if activation == \"relu\" else F.selu\n",
        "#         #输入通道数，图片是灰度图，所以是1，图片是彩色图，就是3，输出通道数，就是卷积核的个数（32,1,28,28）\n",
        "#         self.conv1 = nn.Conv2d(in_channels=1, out_channels=32, kernel_size=3, padding=1)\n",
        "#         #输入x(32,32,28,28) 输出x(32,32,28,28)\n",
        "#         self.conv2 = nn.Conv2d(in_channels=32, out_channels=32, kernel_size=3, padding=1)\n",
        "#         self.pool = nn.MaxPool2d(2, 2) #池化核大小为2（2*2），步长为2\n",
        "#         self.conv3 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3, padding=1)\n",
        "#         self.conv4 = nn.Conv2d(in_channels=64, out_channels=64, kernel_size=3, padding=1)\n",
        "#         self.conv5 = nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, padding=1)\n",
        "#         self.conv6 = nn.Conv2d(in_channels=128, out_channels=128, kernel_size=3, padding=1)\n",
        "#         self.flatten = nn.Flatten()\n",
        "#         # input shape is (28, 28, 1) so the fc1 layer in_features is 128 * 3 * 3\n",
        "#         self.fc1 = nn.Linear(128 * 3 * 3, 128)\n",
        "#         self.fc2 = nn.Linear(128, 10) #输出尺寸（32,10）\n",
        "#\n",
        "#         self.init_weights()\n",
        "#\n",
        "#     def init_weights(self):\n",
        "#         \"\"\"使用 xavier 均匀分布来初始化全连接层、卷积层的权重 W\"\"\"\n",
        "#         for m in self.modules():\n",
        "#             if isinstance(m, (nn.Linear, nn.Conv2d)):\n",
        "#                 nn.init.xavier_uniform_(m.weight)\n",
        "#                 nn.init.zeros_(m.bias)\n",
        "#\n",
        "#     def forward(self, x):\n",
        "#         act = self.activation\n",
        "#         x = self.pool(act(self.conv2(act(self.conv1(x))))) # 1 * 28 * 28 -> 32 * 14 * 14\n",
        "#         print(x.shape)\n",
        "#         x = self.pool(act(self.conv4(act(self.conv3(x))))) # 32 * 14 * 14 -> 64 * 7 * 7\n",
        "#         print(x.shape)\n",
        "#         x = self.pool(act(self.conv6(act(self.conv5(x))))) # 64 * 7 * 7 -> 128 * 3 * 3\n",
        "#         print(x.shape)\n",
        "#         x = self.flatten(x) # 128 * 3 * 3 ->1152\n",
        "#         x = act(self.fc1(x)) # 1152 -> 128\n",
        "#         x = self.fc2(x) # 128 -> 10\n",
        "#         return x\n",
        "#\n",
        "#\n",
        "# for idx, (key, value) in enumerate(CNN().named_parameters()):\n",
        "#     print(f\"{key}\\tparamerters num: {np.prod(value.shape)}\") # 打印模型的参数信息\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "yt7hoBOSWGCD"
      },
      "outputs": [],
      "source": [
        "#练习不同尺寸的卷积核，padding，stride的效果\n",
        "class CNN(nn.Module):\n",
        "    def __init__(self, activation=\"relu\"):\n",
        "        super(CNN, self).__init__()\n",
        "        self.activation = F.relu if activation == \"relu\" else F.selu\n",
        "        #输入通道数，图片是灰度图，所以是1，图片是彩色图，就是3，输出通道数，就是卷积核的个数（32,1,28,28）\n",
        "        self.conv1 = nn.Conv2d(in_channels=1, out_channels=32, kernel_size=5,padding=2,stride=2)\n",
        "        #输入x(32,32,28,28) 输出x(32,32,28,28)\n",
        "        self.conv2 = nn.Conv2d(in_channels=32, out_channels=32, kernel_size=3, padding=1)\n",
        "        self.pool = nn.MaxPool2d(2, 2) #池化核大小为2（2*2），步长为2\n",
        "        self.conv3 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3, padding=1)\n",
        "        self.conv4 = nn.Conv2d(in_channels=64, out_channels=64, kernel_size=3, padding=1)\n",
        "        self.conv5 = nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, padding=1)\n",
        "        self.conv6 = nn.Conv2d(in_channels=128, out_channels=128, kernel_size=3, padding=1)\n",
        "        self.flatten = nn.Flatten()\n",
        "        # input shape is (28, 28, 1) so the fc1 layer in_features is 128 * 3 * 3\n",
        "        self.fc1 = nn.Linear(128 * 3 * 3, 128)\n",
        "        self.fc2 = nn.Linear(128, 10) #输出尺寸（32,10）\n",
        "\n",
        "        self.init_weights()\n",
        "\n",
        "    def init_weights(self):\n",
        "        \"\"\"使用 xavier 均匀分布来初始化全连接层、卷积层的权重 W\"\"\"\n",
        "        for m in self.modules():\n",
        "            if isinstance(m, (nn.Linear, nn.Conv2d)):\n",
        "                nn.init.xavier_uniform_(m.weight)\n",
        "                nn.init.zeros_(m.bias)\n",
        "\n",
        "    def forward(self, x):\n",
        "        act = self.activation\n",
        "        x=act(self.conv1(x)) # 1 * 28 * 28 -> 32 * 28 * 28\n",
        "        print(x.shape)\n",
        "        # x=act(self.conv2(x)) # 32 * 28 * 28 -> 32 * 28 * 28\n",
        "        # print(x.shape)\n",
        "        # x = self.pool(x) # 32 * 28 * 28 -> 32 * 14 * 14\n",
        "        # print(x.shape)\n",
        "        # x=act(self.conv3(x)) # 32 * 14 * 14 -> 64 * 14 * 14\n",
        "        # print(x.shape)\n",
        "        # x=act(self.conv4(x)) # 64 * 14 * 14 -> 64 * 14 * 14\n",
        "        # print(x.shape)\n",
        "        # x = self.pool(x) # 32 * 14 * 14 -> 64 * 7 * 7\n",
        "        # print(x.shape)\n",
        "        # x=act(self.conv5(x)) # 64 * 7 * 7 -> 128 * 7 * 7\n",
        "        # print(x.shape)\n",
        "        # x=act(self.conv6(x)) # 128 * 7 * 7 -> 128 * 7 * 7\n",
        "        # print(x.shape)\n",
        "        # x = self.pool(x) # 128 * 7 * 7 -> 128 * 3 * 3\n",
        "        # print(x.shape)\n",
        "        # x = self.flatten(x) # 128 * 3 * 3 ->1152\n",
        "        # x = act(self.fc1(x)) # 1152 -> 128\n",
        "        # x = self.fc2(x) # 128 -> 10\n",
        "        return x\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "pYNhEWRQWGCD",
        "outputId": "776591fa-152b-4127-b6e1-5a19a441788a"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "torch.Size([1, 32, 14, 14])\n"
          ]
        },
        {
          "data": {
            "text/plain": "tensor([[[[0.0000, 0.1015, 0.0000,  ..., 0.0000, 0.0199, 0.0000],\n          [0.3187, 0.0000, 0.1438,  ..., 0.0000, 0.0878, 0.1333],\n          [0.0000, 0.3048, 0.0520,  ..., 0.2206, 0.0000, 0.0000],\n          ...,\n          [0.0000, 0.5079, 0.0000,  ..., 0.0843, 0.0000, 0.0000],\n          [0.0000, 0.0000, 0.0000,  ..., 0.0000, 0.0000, 0.0000],\n          [0.1897, 0.1022, 0.2356,  ..., 0.2406, 0.0000, 0.1352]],\n\n         [[0.0609, 0.0000, 0.0000,  ..., 0.0000, 0.0000, 0.1504],\n          [0.0000, 0.0000, 0.1017,  ..., 0.0197, 0.2863, 0.0000],\n          [0.0000, 0.4218, 0.1777,  ..., 0.1845, 0.0000, 0.0320],\n          ...,\n          [0.1553, 0.0466, 0.4258,  ..., 0.1250, 0.3502, 0.0000],\n          [0.0000, 0.1878, 0.0000,  ..., 0.0000, 0.1825, 0.3323],\n          [0.0000, 0.0000, 0.0000,  ..., 0.2021, 0.0000, 0.0000]],\n\n         [[0.0000, 0.0947, 0.0000,  ..., 0.0000, 0.0000, 0.2079],\n          [0.1829, 0.0000, 0.2040,  ..., 0.0388, 0.0000, 0.1060],\n          [0.0000, 0.0000, 0.0504,  ..., 0.2619, 0.0000, 0.0000],\n          ...,\n          [0.0000, 0.3649, 0.0744,  ..., 0.3214, 0.0000, 0.0000],\n          [0.0836, 0.3861, 0.2640,  ..., 0.0000, 0.1929, 0.1742],\n          [0.0264, 0.0000, 0.0823,  ..., 0.0000, 0.0000, 0.1422]],\n\n         ...,\n\n         [[0.0000, 0.0000, 0.0000,  ..., 0.1792, 0.0000, 0.0000],\n          [0.1478, 0.0000, 0.0000,  ..., 0.0089, 0.0939, 0.1685],\n          [0.0000, 0.4114, 0.3107,  ..., 0.3608, 0.0728, 0.0000],\n          ...,\n          [0.2956, 0.3582, 0.2190,  ..., 0.0000, 0.3610, 0.3123],\n          [0.0000, 0.0000, 0.0000,  ..., 0.0000, 0.0000, 0.0000],\n          [0.0850, 0.3217, 0.2991,  ..., 0.0087, 0.2128, 0.3534]],\n\n         [[0.0209, 0.0000, 0.2160,  ..., 0.1566, 0.0000, 0.0000],\n          [0.0000, 0.0000, 0.0000,  ..., 0.3342, 0.0000, 0.0000],\n          [0.1394, 0.0000, 0.1181,  ..., 0.0000, 0.1861, 0.0000],\n          ...,\n          [0.4250, 0.0000, 0.0000,  ..., 0.0000, 0.0572, 0.1020],\n          [0.0658, 0.0000, 0.0000,  ..., 0.0315, 0.0000, 0.0522],\n          [0.0000, 0.0298, 0.0000,  ..., 0.0000, 0.3208, 0.1358]],\n\n         [[0.0000, 0.1075, 0.0346,  ..., 0.0000, 0.0000, 0.2186],\n          [0.0000, 0.0000, 0.1961,  ..., 0.0000, 0.0000, 0.0109],\n          [0.0000, 0.0072, 0.0000,  ..., 0.0353, 0.0000, 0.0000],\n          ...,\n          [0.0000, 0.0806, 0.2157,  ..., 0.3526, 0.0019, 0.0000],\n          [0.0000, 0.0000, 0.4120,  ..., 0.0000, 0.1767, 0.1250],\n          [0.0112, 0.0000, 0.0812,  ..., 0.0536, 0.1101, 0.0075]]]],\n       grad_fn=<ReluBackward0>)"
          },
          "execution_count": 15,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "activation = \"relu\"\n",
        "model = CNN(activation)\n",
        "# model.to(device)\n",
        "img = torch.randn(1, 1, 28, 28)\n",
        "model(img)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "Gq1VurSRWGCE",
        "outputId": "85477849-1530-457f-b7cf-af1e060fa5f2"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "conv1.weight\tparamerters num: 288\n",
            "conv1.bias\tparamerters num: 32\n",
            "conv2.weight\tparamerters num: 9216\n",
            "conv2.bias\tparamerters num: 32\n",
            "conv3.weight\tparamerters num: 18432\n",
            "conv3.bias\tparamerters num: 64\n",
            "conv4.weight\tparamerters num: 36864\n",
            "conv4.bias\tparamerters num: 64\n",
            "conv5.weight\tparamerters num: 73728\n",
            "conv5.bias\tparamerters num: 128\n",
            "conv6.weight\tparamerters num: 147456\n",
            "conv6.bias\tparamerters num: 128\n",
            "fc1.weight\tparamerters num: 147456\n",
            "fc1.bias\tparamerters num: 128\n",
            "fc2.weight\tparamerters num: 1280\n",
            "fc2.bias\tparamerters num: 10\n"
          ]
        }
      ],
      "source": [
        "\n",
        "class CNN(nn.Module):\n",
        "    def __init__(self, activation=\"relu\"):\n",
        "        super(CNN, self).__init__()\n",
        "        self.activation = F.relu if activation == \"relu\" else F.selu\n",
        "        #输入通道数，图片是灰度图，所以是1，图片是彩色图，就是3，输出通道数，就是卷积核的个数（32,1,28,28）\n",
        "        self.conv1 = nn.Conv2d(in_channels=1, out_channels=32, kernel_size=3, padding=1)\n",
        "        #输入x(32,32,28,28) 输出x(32,32,28,28)\n",
        "        self.conv2 = nn.Conv2d(in_channels=32, out_channels=32, kernel_size=3, padding=1)\n",
        "        self.pool = nn.MaxPool2d(2, 2) #池化核大小为2（2*2），步长为2\n",
        "        self.conv3 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3, padding=1)\n",
        "        self.conv4 = nn.Conv2d(in_channels=64, out_channels=64, kernel_size=3, padding=1)\n",
        "        self.conv5 = nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, padding=1)\n",
        "        self.conv6 = nn.Conv2d(in_channels=128, out_channels=128, kernel_size=3, padding=1)\n",
        "        self.flatten = nn.Flatten()\n",
        "        # input shape is (28, 28, 1) so the fc1 layer in_features is 128 * 3 * 3\n",
        "        self.fc1 = nn.Linear(128 * 3 * 3, 128)\n",
        "        self.fc2 = nn.Linear(128, 10) #输出尺寸（32,10）\n",
        "\n",
        "        self.init_weights()\n",
        "\n",
        "    def init_weights(self):\n",
        "        \"\"\"使用 xavier 均匀分布来初始化全连接层、卷积层的权重 W\"\"\"\n",
        "        for m in self.modules():\n",
        "            if isinstance(m, (nn.Linear, nn.Conv2d)):\n",
        "                nn.init.xavier_uniform_(m.weight)\n",
        "                nn.init.zeros_(m.bias)\n",
        "\n",
        "    def forward(self, x):\n",
        "        act = self.activation\n",
        "        x=act(self.conv1(x)) # 1 * 28 * 28 -> 32 * 28 * 28\n",
        "        print(x.shape)\n",
        "        x=act(self.conv2(x)) # 32 * 28 * 28 -> 32 * 28 * 28\n",
        "        print(x.shape)\n",
        "        x = self.pool(x) # 32 * 28 * 28 -> 32 * 14 * 14\n",
        "        print(x.shape)\n",
        "        x=act(self.conv3(x)) # 32 * 14 * 14 -> 64 * 14 * 14\n",
        "        print(x.shape)\n",
        "        x=act(self.conv4(x)) # 64 * 14 * 14 -> 64 * 14 * 14\n",
        "        print(x.shape)\n",
        "        x = self.pool(x) # 32 * 14 * 14 -> 64 * 7 * 7\n",
        "        print(x.shape)\n",
        "        x=act(self.conv5(x)) # 64 * 7 * 7 -> 128 * 7 * 7\n",
        "        print(x.shape)\n",
        "        x=act(self.conv6(x)) # 128 * 7 * 7 -> 128 * 7 * 7\n",
        "        print(x.shape)\n",
        "        x = self.pool(x) # 128 * 7 * 7 -> 128 * 3 * 3\n",
        "        print(x.shape)\n",
        "        x = self.flatten(x) # 128 * 3 * 3 ->1152\n",
        "        x = act(self.fc1(x)) # 1152 -> 128\n",
        "        x = self.fc2(x) # 128 -> 10\n",
        "        return x\n",
        "\n",
        "\n",
        "for idx, (key, value) in enumerate(CNN().named_parameters()):\n",
        "    print(f\"{key}\\tparamerters num: {np.prod(value.shape)}\") # 打印模型的参数信息\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "gUxu_50GWGCE"
      },
      "outputs": [],
      "source": [
        "activation = \"relu\"\n",
        "model = CNN(activation)\n",
        "# model.to(device)\n",
        "# img = torch.randn(1, 1, 28, 28)\n",
        "# model(img)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "9T61HWhKWGCE",
        "outputId": "064e4f40-124b-4842-9d78-731a81970888"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "torch.Size([1, 32, 28, 28])\n",
            "torch.Size([1, 32, 28, 28])\n",
            "torch.Size([1, 32, 14, 14])\n",
            "torch.Size([1, 64, 14, 14])\n",
            "torch.Size([1, 64, 14, 14])\n",
            "torch.Size([1, 64, 7, 7])\n",
            "torch.Size([1, 128, 7, 7])\n",
            "torch.Size([1, 128, 7, 7])\n",
            "torch.Size([1, 128, 3, 3])\n"
          ]
        },
        {
          "data": {
            "text/plain": "'model_CNN.png'"
          },
          "execution_count": 4,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "from torchviz import make_dot\n",
        "\n",
        "# Assuming your model is already defined and named 'model'\n",
        "# Construct a dummy input\n",
        "dummy_input = torch.randn(1, 1, 28, 28)  # Replace with your input shape\n",
        "\n",
        "# Forward pass to generate the computation graph\n",
        "output = model(dummy_input)\n",
        "\n",
        "# Visualize the model architecture\n",
        "dot = make_dot(output, params=dict(model.named_parameters()))\n",
        "dot.render(\"model_CNN\", format=\"png\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "fJeWn6SFWGCE",
        "outputId": "98634dce-e48f-41c8-ec46-3835aedd6ccf"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "conv1 - 288\n",
            "conv2 - 9216\n",
            "conv3 - 18432\n",
            "conv4 - 36864\n",
            "conv5 - 73728\n",
            "conv6 - 147456\n",
            "fc1 - 147456\n",
            "fc2 - 1280\n"
          ]
        },
        {
          "data": {
            "text/plain": "435306"
          },
          "execution_count": 9,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "#计算参数量\n",
        "print(f'conv1 - {1*3*3*32}') # 32个卷积核，每个卷积核大小为1*3*3\n",
        "print(f'conv2 - {32*3*3*32}') # 32个卷积核，每个卷积核大小为32*3*3\n",
        "print(f'conv3 - {32*3*3*64}')\n",
        "print(f'conv4 - {64*3*3*64}')\n",
        "print(f'conv5 - {64*3*3*128}')\n",
        "print(f'conv6 - {128*3*3*128}')\n",
        "print(f'fc1 - {1152*128}')\n",
        "print(f'fc2 - {128*10}')\n",
        "\n",
        "#对上面求和，总参数数目为：\n",
        "1*3*3*32 +32+ 32*3*3*32 +32+ 32*3*3*64 +64+ 64*3*3*64+64 + 64*3*3*128 +128+ 128*3*3*128 +128+ 128*3*3*128+128 + 128*10+10"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "8sZqvQYLWGCF"
      },
      "source": [
        "## 训练\n",
        "\n",
        "pytorch的训练需要自行实现，包括\n",
        "1. 定义损失函数\n",
        "2. 定义优化器\n",
        "3. 定义训练步\n",
        "4. 训练"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "Sgwi5T9zWGCF"
      },
      "outputs": [],
      "source": [
        "from sklearn.metrics import accuracy_score\n",
        "\n",
        "@torch.no_grad()\n",
        "def evaluating(model, dataloader, loss_fct):\n",
        "    loss_list = []\n",
        "    pred_list = []\n",
        "    label_list = []\n",
        "    for datas, labels in dataloader:\n",
        "        datas = datas.to(device)\n",
        "        labels = labels.to(device)\n",
        "        # 前向计算\n",
        "        logits = model(datas)              # 验证集预测\n",
        "        loss = loss_fct(logits, labels)         # 验证集损失\n",
        "        loss_list.append(loss.item()) # 将验证集损失加入列表\n",
        "\n",
        "        preds = logits.argmax(axis=-1)    # 验证集预测\n",
        "        pred_list.extend(preds.cpu().numpy().tolist()) # 将验证集预测结果加入列表\n",
        "        label_list.extend(labels.cpu().numpy().tolist())# 将验证集真实标签加入列表\n",
        "\n",
        "    acc = accuracy_score(label_list, pred_list) # 计算验证集准确率\n",
        "    return np.mean(loss_list), acc # 返回验证集损失均值和准确率\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "OeXZk-gJWGCF"
      },
      "source": [
        "### TensorBoard 可视化\n",
        "\n",
        "\n",
        "训练过程中可以使用如下命令启动tensorboard服务。\n",
        "\n",
        "```shell\n",
        "tensorboard \\\n",
        "    --logdir=runs \\     # log 存放路径\n",
        "    --host 0.0.0.0 \\    # ip\n",
        "    --port 8848         # 端口\n",
        "```"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "HVxlMyqdWGCF"
      },
      "outputs": [],
      "source": [
        "from torch.utils.tensorboard import SummaryWriter\n",
        "\n",
        "\n",
        "class TensorBoardCallback:\n",
        "    def __init__(self, log_dir, flush_secs=10):\n",
        "        \"\"\"\n",
        "        Args:\n",
        "            log_dir (str): dir to write log.\n",
        "            flush_secs (int, optional): write to dsk each flush_secs seconds. Defaults to 10.\n",
        "        \"\"\"\n",
        "        self.writer = SummaryWriter(log_dir=log_dir, flush_secs=flush_secs)\n",
        "\n",
        "    def draw_model(self, model, input_shape):\n",
        "        self.writer.add_graph(model, input_to_model=torch.randn(input_shape))\n",
        "\n",
        "    def add_loss_scalars(self, step, loss, val_loss):\n",
        "        self.writer.add_scalars(\n",
        "            main_tag=\"training/loss\",\n",
        "            tag_scalar_dict={\"loss\": loss, \"val_loss\": val_loss},\n",
        "            global_step=step,\n",
        "            )\n",
        "\n",
        "    def add_acc_scalars(self, step, acc, val_acc):\n",
        "        self.writer.add_scalars(\n",
        "            main_tag=\"training/accuracy\",\n",
        "            tag_scalar_dict={\"accuracy\": acc, \"val_accuracy\": val_acc},\n",
        "            global_step=step,\n",
        "        )\n",
        "\n",
        "    def add_lr_scalars(self, step, learning_rate):\n",
        "        self.writer.add_scalars(\n",
        "            main_tag=\"training/learning_rate\",\n",
        "            tag_scalar_dict={\"learning_rate\": learning_rate},\n",
        "            global_step=step,\n",
        "\n",
        "        )\n",
        "\n",
        "    def __call__(self, step, **kwargs):\n",
        "        # add loss\n",
        "        loss = kwargs.pop(\"loss\", None)\n",
        "        val_loss = kwargs.pop(\"val_loss\", None)\n",
        "        if loss is not None and val_loss is not None:\n",
        "            self.add_loss_scalars(step, loss, val_loss)\n",
        "        # add acc\n",
        "        acc = kwargs.pop(\"acc\", None)\n",
        "        val_acc = kwargs.pop(\"val_acc\", None)\n",
        "        if acc is not None and val_acc is not None:\n",
        "            self.add_acc_scalars(step, acc, val_acc)\n",
        "        # add lr\n",
        "        learning_rate = kwargs.pop(\"lr\", None)\n",
        "        if learning_rate is not None:\n",
        "            self.add_lr_scalars(step, learning_rate)\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "4cl1WTT7WGCF"
      },
      "source": [
        "### Save Best\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "otM2WK0eWGCF"
      },
      "outputs": [],
      "source": [
        "class SaveCheckpointsCallback:\n",
        "    def __init__(self, save_dir, save_step=5000, save_best_only=True):\n",
        "        \"\"\"\n",
        "        Save checkpoints each save_epoch epoch.\n",
        "        We save checkpoint by epoch in this implementation.\n",
        "        Usually, training scripts with pytorch evaluating model and save checkpoint by step.\n",
        "\n",
        "        Args:\n",
        "            save_dir (str): dir to save checkpoint\n",
        "            save_epoch (int, optional): the frequency to save checkpoint. Defaults to 1.\n",
        "            save_best_only (bool, optional): If True, only save the best model or save each model at every epoch.\n",
        "        \"\"\"\n",
        "        self.save_dir = save_dir\n",
        "        self.save_step = save_step\n",
        "        self.save_best_only = save_best_only\n",
        "        self.best_metrics = -1\n",
        "\n",
        "        # mkdir\n",
        "        if not os.path.exists(self.save_dir):\n",
        "            os.mkdir(self.save_dir)\n",
        "\n",
        "    def __call__(self, step, state_dict, metric=None):\n",
        "        if step % self.save_step > 0:\n",
        "            return\n",
        "\n",
        "        if self.save_best_only:\n",
        "            assert metric is not None\n",
        "            if metric >= self.best_metrics:\n",
        "                # save checkpoints\n",
        "                torch.save(state_dict, os.path.join(self.save_dir, \"best.ckpt\"))\n",
        "                # update best metrics\n",
        "                self.best_metrics = metric\n",
        "        else:\n",
        "            torch.save(state_dict, os.path.join(self.save_dir, f\"{step}.ckpt\"))\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "TB-mR0jTWGCG"
      },
      "source": [
        "### Early Stop"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "BZg_3ddDWGCG"
      },
      "outputs": [],
      "source": [
        "class EarlyStopCallback:\n",
        "    def __init__(self, patience=5, min_delta=0.01):\n",
        "        \"\"\"\n",
        "\n",
        "        Args:\n",
        "            patience (int, optional): Number of epochs with no improvement after which training will be stopped.. Defaults to 5.\n",
        "            min_delta (float, optional): Minimum change in the monitored quantity to qualify as an improvement, i.e. an absolute\n",
        "                change of less than min_delta, will count as no improvement. Defaults to 0.01.\n",
        "        \"\"\"\n",
        "        self.patience = patience\n",
        "        self.min_delta = min_delta\n",
        "        self.best_metric = -1\n",
        "        self.counter = 0\n",
        "\n",
        "    def __call__(self, metric):\n",
        "        if metric >= self.best_metric + self.min_delta:\n",
        "            # update best metric\n",
        "            self.best_metric = metric\n",
        "            # reset counter\n",
        "            self.counter = 0\n",
        "        else:\n",
        "            self.counter += 1\n",
        "\n",
        "    @property\n",
        "    def early_stop(self):\n",
        "        return self.counter >= self.patience\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "lGXbNGtXWGCG",
        "outputId": "54264090-f626-4959-b798-d4979f88f8c9"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "torch.Size([1, 32, 28, 28])\n",
            "torch.Size([1, 32, 28, 28])\n",
            "torch.Size([1, 32, 14, 14])\n",
            "torch.Size([1, 64, 14, 14])\n",
            "torch.Size([1, 64, 14, 14])\n",
            "torch.Size([1, 64, 7, 7])\n",
            "torch.Size([1, 128, 7, 7])\n",
            "torch.Size([1, 128, 7, 7])\n",
            "torch.Size([1, 128, 3, 3])\n",
            "torch.Size([1, 32, 28, 28])\n",
            "torch.Size([1, 32, 28, 28])\n",
            "torch.Size([1, 32, 14, 14])\n",
            "torch.Size([1, 64, 14, 14])\n",
            "torch.Size([1, 64, 14, 14])\n",
            "torch.Size([1, 64, 7, 7])\n",
            "torch.Size([1, 128, 7, 7])\n",
            "torch.Size([1, 128, 7, 7])\n",
            "torch.Size([1, 128, 3, 3])\n",
            "torch.Size([1, 32, 28, 28])\n",
            "torch.Size([1, 32, 28, 28])\n",
            "torch.Size([1, 32, 14, 14])\n",
            "torch.Size([1, 64, 14, 14])\n",
            "torch.Size([1, 64, 14, 14])\n",
            "torch.Size([1, 64, 7, 7])\n",
            "torch.Size([1, 128, 7, 7])\n",
            "torch.Size([1, 128, 7, 7])\n",
            "torch.Size([1, 128, 3, 3])\n"
          ]
        }
      ],
      "source": [
        "# 训练\n",
        "def training(\n",
        "    model,\n",
        "    train_loader,\n",
        "    val_loader,\n",
        "    epoch,\n",
        "    loss_fct,\n",
        "    optimizer,\n",
        "    tensorboard_callback=None,\n",
        "    save_ckpt_callback=None,\n",
        "    early_stop_callback=None,\n",
        "    eval_step=500,\n",
        "    ):\n",
        "    record_dict = {\n",
        "        \"train\": [],\n",
        "        \"val\": []\n",
        "    }\n",
        "\n",
        "    global_step = 0\n",
        "    model.train()\n",
        "    with tqdm(total=epoch * len(train_loader)) as pbar:\n",
        "        for epoch_id in range(epoch):\n",
        "            # training\n",
        "            for datas, labels in train_loader:\n",
        "                datas = datas.to(device)\n",
        "                labels = labels.to(device)\n",
        "                # 梯度清空\n",
        "                optimizer.zero_grad()\n",
        "                # 模型前向计算\n",
        "                logits = model(datas)\n",
        "                # 计算损失\n",
        "                loss = loss_fct(logits, labels)\n",
        "                # 梯度回传\n",
        "                loss.backward()\n",
        "                # 调整优化器，包括学习率的变动等\n",
        "                optimizer.step()\n",
        "                preds = logits.argmax(axis=-1)\n",
        "\n",
        "                acc = accuracy_score(labels.cpu().numpy(), preds.cpu().numpy())\n",
        "                loss = loss.cpu().item()\n",
        "                # record\n",
        "\n",
        "                record_dict[\"train\"].append({\n",
        "                    \"loss\": loss, \"acc\": acc, \"step\": global_step\n",
        "                })\n",
        "\n",
        "                # evaluating\n",
        "                if global_step % eval_step == 0:\n",
        "                    model.eval()\n",
        "                    val_loss, val_acc = evaluating(model, val_loader, loss_fct)\n",
        "                    record_dict[\"val\"].append({\n",
        "                        \"loss\": val_loss, \"acc\": val_acc, \"step\": global_step\n",
        "                    })\n",
        "                    model.train()\n",
        "\n",
        "                    # 1. 使用 tensorboard 可视化\n",
        "                    if tensorboard_callback is not None:\n",
        "                        tensorboard_callback(\n",
        "                            global_step,\n",
        "                            loss=loss, val_loss=val_loss,\n",
        "                            acc=acc, val_acc=val_acc,\n",
        "                            lr=optimizer.param_groups[0][\"lr\"],\n",
        "                            )\n",
        "\n",
        "                    # 2. 保存模型权重 save model checkpoint\n",
        "                    if save_ckpt_callback is not None:\n",
        "                        save_ckpt_callback(global_step, model.state_dict(), metric=val_acc)\n",
        "\n",
        "                    # 3. 早停 Early Stop\n",
        "                    if early_stop_callback is not None:\n",
        "                        early_stop_callback(val_acc)\n",
        "                        if early_stop_callback.early_stop:\n",
        "                            print(f\"Early stop at epoch {epoch_id} / global_step {global_step}\")\n",
        "                            return record_dict\n",
        "\n",
        "                # udate step\n",
        "                global_step += 1\n",
        "                pbar.update(1)\n",
        "                pbar.set_postfix({\"epoch\": epoch_id})\n",
        "\n",
        "    return record_dict\n",
        "\n",
        "\n",
        "epoch = 20\n",
        "\n",
        "activation = \"relu\"\n",
        "model = CNN(activation)\n",
        "\n",
        "# 1. 定义损失函数 采用交叉熵损失\n",
        "loss_fct = nn.CrossEntropyLoss()\n",
        "# 2. 定义优化器 采用SGD\n",
        "# Optimizers specified in the torch.optim package\n",
        "optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.9)\n",
        "\n",
        "# 1. tensorboard 可视化\n",
        "if not os.path.exists(\"runs\"):\n",
        "    os.mkdir(\"runs\")\n",
        "tensorboard_callback = TensorBoardCallback(f\"runs/cnn-{activation}\")\n",
        "tensorboard_callback.draw_model(model, [1, 1, 28, 28])\n",
        "# 2. save best\n",
        "if not os.path.exists(\"checkpoints\"):\n",
        "    os.makedirs(\"checkpoints\")\n",
        "save_ckpt_callback = SaveCheckpointsCallback(f\"checkpoints/cnn-{activation}\", save_best_only=True)\n",
        "# 3. early stop\n",
        "early_stop_callback = EarlyStopCallback(patience=10)\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "1E0aKNfJWGCG"
      },
      "outputs": [],
      "source": [
        "model = model.to(device)\n",
        "record = training(\n",
        "    model,\n",
        "    train_loader,\n",
        "    val_loader,\n",
        "    epoch,\n",
        "    loss_fct,\n",
        "    optimizer,\n",
        "    tensorboard_callback=tensorboard_callback,\n",
        "    save_ckpt_callback=save_ckpt_callback,\n",
        "    early_stop_callback=early_stop_callback,\n",
        "    eval_step=1000\n",
        "    )"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "96LC4E2pWGCG",
        "outputId": "7d4cda91-3752-4095-fb71-20dec73fd4dc"
      },
      "outputs": [
        {
          "data": {
            "image/png": "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\n",
            "text/plain": [
              "<Figure size 720x360 with 2 Axes>"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "#画线要注意的是损失是不一定在零到1之间的\n",
        "def plot_learning_curves(record_dict, sample_step=500):\n",
        "    # build DataFrame\n",
        "    train_df = pd.DataFrame(record_dict[\"train\"]).set_index(\"step\").iloc[::sample_step]\n",
        "    val_df = pd.DataFrame(record_dict[\"val\"]).set_index(\"step\")\n",
        "\n",
        "    # plot\n",
        "    fig_num = len(train_df.columns)\n",
        "    fig, axs = plt.subplots(1, fig_num, figsize=(5 * fig_num, 5))\n",
        "    for idx, item in enumerate(train_df.columns):\n",
        "        axs[idx].plot(train_df.index, train_df[item], label=f\"train_{item}\")\n",
        "        axs[idx].plot(val_df.index, val_df[item], label=f\"val_{item}\")\n",
        "        axs[idx].grid()\n",
        "        axs[idx].legend()\n",
        "        axs[idx].set_xticks(range(0, train_df.index[-1], 5000))\n",
        "        axs[idx].set_xticklabels(map(lambda x: f\"{int(x/1000)}k\", range(0, train_df.index[-1], 5000)))\n",
        "        axs[idx].set_xlabel(\"step\")\n",
        "\n",
        "    plt.show()\n",
        "\n",
        "plot_learning_curves(record, sample_step=500)  #横坐标是 steps"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "hy5nxmc-WGCH"
      },
      "source": [
        "# 评估"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "6xOAq4AgWGCH",
        "outputId": "ca5fbb41-af80-491e-9284-084594fb6014"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "loss:     0.2904\n",
            "accuracy: 0.9031\n"
          ]
        }
      ],
      "source": [
        "# dataload for evaluating\n",
        "\n",
        "# load checkpoints\n",
        "model.load_state_dict(torch.load(f\"checkpoints/cnn-{activation}/best.ckpt\", map_location=\"cpu\"))\n",
        "\n",
        "model.eval()\n",
        "loss, acc = evaluating(model, test_loader, loss_fct)\n",
        "print(f\"loss:     {loss:.4f}\\naccuracy: {acc:.4f}\")"
      ]
    }
  ],
  "metadata": {
    "accelerator": "GPU",
    "colab": {
      "gpuType": "T4",
      "provenance": []
    },
    "kernelspec": {
      "display_name": "Python 3",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3.12.3"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}